mod basic_type;
mod config;
mod disassemble;
mod probe;
mod return_value;
mod timer;
mod util;
mod value_writer;

pub use config::*;
use disassemble::*;
pub(crate) use probe::*;
use return_value::*;
use util::*;
pub use value_writer::*;

use crate::{
    value::{read_str, RVal, Val},
    version::rustc_version,
    ActiveFrame, AllocAction, AllocationBorrowed, Breakpoint, BreakpointType, Bytes, EventStream,
    SourceFile, UnionType, VariableCapture, WriteErr, ALLOCATION_STREAM, BREAKPOINT_STREAM,
    EVENT_STREAM, FILE_STREAM, INFO_STREAM,
};
use anyhow::{Context, Result};
use firedbg_protocol::{
    event::Reason,
    info::{InfoMessage, ProgExitInfo},
};
use firedbg_rust_parser::FunctionDef;
use lldb::{
    IsValid, ProcessState, SBAddress, SBBreakpoint, SBData, SBDebugger, SBFunctionId, SBProcess,
    SBSymbolId, SBTarget, SBThread, SBType, SBTypeId, SBValue, StopReason, VariableOptions,
};
use rustc_hash::{FxHashMap, FxHashSet};
use sea_streamer::{Producer, SeaProducer, SeaStreamerBackend, StreamKey};
use std::{
    path::Path,
    sync::{Arc, Mutex},
    time::SystemTime,
};

static mut SB_TARGET: Option<SBTarget> = None;
static mut SB_PROCESS: Option<SBProcess> = None;

lazy_static::lazy_static! {
    static ref ENUM_CACHE: Mutex<FxHashMap<SBTypeId, Option<Arc<UnionType>>>> = Mutex::new(Default::default());
    static ref TYPE_CACHE: Mutex<FxHashMap<String, Option<SBType>>> = Mutex::new(Default::default());
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// Debugger Parameters
pub struct DebuggerParams {
    /// The executable
    pub binary: String,
    /// List of source files
    pub files: Vec<SourceFile>,
    /// List of breakpoints
    pub breakpoints: Vec<Breakpoint>,
    /// Arguments to pass through to the executable
    pub arguments: Vec<String>,
}

#[derive(Debug)]
struct Thread {
    frame: Frame,
    active_frames: Vec<ActiveFrame>,
    allocating: Option<String>,
}

#[derive(Debug)]
struct Frame {
    frame_id: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// The Debugger Engine
pub struct Debugger {}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
/// Breakpoint ID
pub struct BpId(pub u32);

pub const FIREDBG_SOURCE_FILE_ID: u32 = 0;
pub const RUST_PANIC_BP_ID: BpId = BpId(1);
pub const FIREDBG_TRACE_BP_ID: BpId = BpId(2);
pub const EXCHANGE_MALLOC: BpId = BpId(3);
pub const DROP_IN_PLACE: BpId = BpId(4);

const RET: &str = {
    #[cfg(target_arch = "x86_64")]
    {
        "retq"
    }
    #[cfg(target_arch = "aarch64")]
    {
        "ret"
    }
};

/// This value is generated by `build.rs`.
pub const SUPPORTED_RUSTC_VERSION: &str = env!("RUSTC_VERSION");

#[derive(Default)]
struct FunctionCache {
    // Key: sb_function.id()
    // Value: sb_function.get_type().function_return_type() => sb_type
    cache: FxHashMap<SBFunctionId, SBType>,
}

/// Info extracted after disassemble
struct FuncAsm {
    /// byte offset
    extended_prologue: u16,
}

impl FunctionCache {
    fn get(&self, sb_fn_id: &SBFunctionId) -> Option<&SBType> {
        self.cache.get(sb_fn_id)
    }

    fn insert<'a, F>(&'a mut self, sb_fn_id: &SBFunctionId, get_type: F)
    where
        F: FnOnce() -> SBType,
    {
        if !self.cache.contains_key(sb_fn_id) {
            let ty = get_type();
            self.cache.insert(*sb_fn_id, ty);
        }
    }
}

impl Debugger {
    #[inline]
    /// Run the debugger. There can only be one debugger instance per process at any time.
    /// We should not re-run the debugger against a different executable target.
    pub fn run(params: DebuggerParams, producer: SeaProducer) {
        run(params, producer).expect("Fail to start debugger");
    }
}

fn run(mut params: DebuggerParams, mut producer: SeaProducer) -> Result<()> {
    let mut process_timer = timer::ProcessTimer::default();

    let t_global = process_timer.global.span();
    let t_init = process_timer.init.span();

    load_config();
    SBDebugger::initialize();

    let sb_debugger = SBDebugger::create(false);
    sb_debugger.set_async_mode(false);
    log::debug!("{:?}", sb_debugger);

    let path = Path::new(&params.binary);
    let sb_target = sb_debugger
        .create_target(path, None, None, false)
        .with_context(|| format!("Fail to create target: `{}`", path.display()))?;
    let sb_target = unsafe {
        SB_TARGET = Some(sb_target);
        SB_TARGET.as_ref().expect("Always")
    };
    log::debug!("{:?}", sb_target);

    let target_basename = match producer.get_file() {
        Some(file) => get_target_basename(file.file_id().path()),
        None => panic!("Not SeaProducerBackend::File"),
    };
    let stdout_path = &format!("{target_basename}.stdout");
    let stderr_path = &format!("{target_basename}.stderr");

    let mut threads: FxHashMap<u64, Thread> = Default::default();
    let mut breakpoints = vec![Default::default()];
    // PC address
    let mut functions_disassembled: FxHashMap<u64, FuncAsm> = Default::default();
    // File address -> Breakpoint
    let mut breakpoint_addresses: FxHashMap<u64, BpId> = Default::default();
    // memory address -> type name
    let mut allocation: FxHashMap<u64, String> = Default::default();
    let mut fn_cache: FunctionCache = Default::default();

    let bp_stream = StreamKey::new(BREAKPOINT_STREAM)?;
    let event_stream = StreamKey::new(EVENT_STREAM)?;
    let alloc_stream = StreamKey::new(ALLOCATION_STREAM)?;

    let send_breakpoint = |bp: &Breakpoint| -> Result<()> {
        producer
            .send_to(&bp_stream, serde_json::to_string(bp)?)
            .context("Fail to send breakpoint")?;
        Ok(())
    };

    {
        // Set `rust_panic` (dummy) source file
        params.files[0] = get_rust_panic_source_file();

        let file_stream = StreamKey::new(FILE_STREAM)
            .with_context(|| format!("Fail to create StreamKey: `{FILE_STREAM}`"))?;
        for (i, file) in params.files.iter().enumerate() {
            assert_eq!(i as u32, file.id);
            let content = serde_json::to_string(&file).context("Fail to serialize JSON")?;
            producer
                .send_to(&file_stream, content)
                .context("Fail to stream file")?;
        }
    }

    std::mem::drop(t_init); // end timer

    let t_set_breakpoint = process_timer.set_breakpoint.span();

    let sb_bp = sb_target.breakpoint_create_by_name("rust_panic");
    let breakpoint = get_rust_panic_breakpoint();
    send_breakpoint(&breakpoint)?;
    register_breakpoint(&mut breakpoint_addresses, &sb_bp);
    breakpoints.push(breakpoint);

    let sb_bp = sb_target.breakpoint_create_by_regex("firedbg_lib::__firedbg_trace__");
    let breakpoint = get_firedbg_trace_breakpoint();
    send_breakpoint(&breakpoint)?;
    register_breakpoint(&mut breakpoint_addresses, &sb_bp);
    breakpoints.push(breakpoint);

    if !*DONT_TRACE_ALLOCATION {
        let sb_bp = sb_target.breakpoint_create_by_name("alloc::alloc::exchange_malloc");
        let breakpoint = get_exchange_malloc_breakpoint();
        send_breakpoint(&breakpoint)?;
        register_breakpoint(&mut breakpoint_addresses, &sb_bp);
        breakpoints.push(breakpoint);

        let sb_bp = sb_target.breakpoint_create_by_name("core::ptr::drop_in_place");
        let breakpoint = get_drop_in_place_breakpoint();
        send_breakpoint(&breakpoint)?;
        register_breakpoint(&mut breakpoint_addresses, &sb_bp);
        breakpoints.push(breakpoint);
    }

    for (i, mut bp) in params.breakpoints.into_iter().enumerate().skip(1) {
        let crate_name = format!("{}::", &params.files[bp.file_id as usize].crate_name);
        assert_eq!(i as u32, bp.id);
        let src = Path::new(&params.files[bp.file_id as usize].path);
        let mut sb_bp = sb_target.breakpoint_create_by_location(&src, bp.loc.line, bp.loc.column);
        log::debug!("{:#?}", sb_bp);
        let mut try_loc_end = false;
        for sb_bp_loc in sb_bp.locations() {
            let desc = format!("{:#?}", sb_bp_loc);
            log::debug!("{}", desc);
            if let Some(fn_full_name) = get_bp_fn_full_name(&desc) {
                if let BreakpointType::FunctionCall { fn_name } = &bp.breakpoint_type {
                    // `fn_full_name` looks like:
                    //   - `hyphenated_main::pet::Cat as core::fmt::Display>::fmt`
                    //   - `T as gen_where::PrintInOption>::print_in_option`
                    //   - `gen_where::main`
                    if (fn_full_name.starts_with(&crate_name) || fn_full_name.contains(&crate_name))
                        && fn_full_name.ends_with(fn_name)
                    {
                        continue;
                    }
                    if is_executable_fn(&desc) && fn_full_name.ends_with(fn_name) {
                        continue;
                    }
                    try_loc_end = true;
                    break;
                }
            }
        }
        if sb_bp.num_locations() == 0 {
            try_loc_end = true;
        }
        if try_loc_end {
            sb_target.breakpoint_delete(breakpoints.len() as u32);
            if let Some(loc_end) = &bp.loc_end {
                sb_bp = sb_target.breakpoint_create_by_location(&src, loc_end.line, loc_end.column);
                log::debug!("{:#?}", sb_bp);
                if log::log_enabled!(log::Level::Debug) {
                    for sb_bp_loc in sb_bp.locations() {
                        log::debug!("{:#?}", sb_bp_loc);
                    }
                }
                breakpoints.push(Default::default());
            }
        }
        register_breakpoint(&mut breakpoint_addresses, &sb_bp);
        // we have to make sure that calling this function once would only hit the breakpoint once
        // disable duplicated breakpoint locations, leaving the first one untouched
        let mut map = FxHashSet::<SBSymbolId>::default();
        for sb_bp_loc in sb_bp.locations() {
            let addr = sb_bp_loc.address();
            let symbol = addr.symbol().expect("Fail to fetch symbol by address");
            let symbol_id = symbol.id();
            if map.contains(&symbol_id) {
                sb_bp_loc.set_enabled(false);
            } else {
                map.insert(symbol_id);
            }
        }
        bp.id = breakpoints.len() as u32;
        if sb_bp.num_locations() > 0 {
            send_breakpoint(&bp)?;
        }
        breakpoints.push(bp);
    }

    std::mem::drop(t_set_breakpoint);

    let mut handle_breakpoint = |breakpoint_addresses: &mut FxHashMap<u64, BpId>,
                                 sb_process: &SBProcess,
                                 sb_thread: SBThread,
                                 bp_id: BpId|
     -> Result<()> {
        let _t = process_timer.handle_breakpoint.span();

        let bp_origin = &breakpoints[bp_id.0 as usize];
        assert_eq!(bp_origin.id, bp_id.0);
        let bp_file_id = bp_origin.file_id;
        let bp_event_type = &bp_origin.breakpoint_type;
        let bp_capture = bp_origin.capture.clone();
        let thread_id = sb_thread.thread_id();
        let mut sb_frame = sb_thread.selected_frame();

        // If frame #0 is inlined,
        // we find the closest non-inlined frame to parse,
        // starting from frame #1, then frame #2... and so on
        let mut frame_idx = 1;
        while sb_frame.is_inlined() && frame_idx < sb_thread.num_frames() {
            sb_frame = sb_thread.set_selected_frame(frame_idx);
            frame_idx += 1;
        }

        let mut rwriter = RValueWriter::new(&allocation);
        let rwriter = &mut rwriter;

        let Thread {
            frame,
            active_frames,
            allocating,
        } = threads.entry(thread_id).or_default();

        log::trace!("= Active Frame (thread={thread_id}) =");
        for af in active_frames.iter() {
            log::trace!("sp = {}", af.stack_pointer);
        }
        log::trace!(" --> {}", sb_frame.sp());

        let mut unwind_last_active_frame = |active_frame: &ActiveFrame| {
            for sb_fra in sb_thread.frames() {
                if sb_fra.sp() > active_frame.stack_pointer {
                    break;
                }
                sb_frame = sb_fra;
                if sb_frame.sp() == active_frame.stack_pointer {
                    break;
                }
            }
            log::warn!(
                "panic! active frame = {}",
                sb_frame.function_name().unwrap_or_default()
            );
        };

        let is_function_return = matches!(bp_event_type, BreakpointType::FunctionReturn);
        let mut return_immediately = false;
        if matches!(
            bp_event_type,
            BreakpointType::Breakpoint
                | BreakpointType::FunctionCall { .. }
                | BreakpointType::FutureEndpoint
        ) {
            let mut event = if matches!(bp_event_type, BreakpointType::Breakpoint) {
                let (active_frame_id, reason) = active_frames
                    .last()
                    .map(|active_frame| {
                        let reason = match bp_id {
                            RUST_PANIC_BP_ID => {
                                unwind_last_active_frame(active_frame);
                                Reason::Panic
                            }
                            _ => Reason::Breakpoint,
                        };
                        (active_frame.frame_id, reason)
                    })
                    .unwrap_or_default();
                EventStream::breakpoint(bp_id, thread_id, active_frame_id, reason)
            } else if matches!(bp_event_type, BreakpointType::FunctionCall { .. }) {
                let sb_function = sb_frame.function();
                let get_return_type = || {
                    let ty = sb_function.type_().function_return_type();
                    log::debug!("{}() -> {}", sb_function.name(), ty.name());
                    ty
                };
                fn_cache.insert(&sb_function.id(), get_return_type);

                if let Some(active_frame) = active_frames.last() {
                    // Hit the same function call breakpoint before function returning
                    if sb_frame.pc() == active_frame.program_counter
                        && sb_frame.sp() == active_frame.stack_pointer
                    {
                        // Skip breakpoint handling
                        return Ok(());
                    }
                    // SP goes deeper
                    assert!(sb_frame.sp() <= active_frame.stack_pointer);
                }
                frame.frame_id += 1;

                // Push a new frame
                active_frames.push(ActiveFrame {
                    frame_id: frame.frame_id,
                    stack_pointer: sb_frame.sp(),
                    program_counter: sb_frame.pc(),
                    function_name: sb_function.name().to_owned(),
                    function_id: sb_function.id(),
                });

                // # Why do we need to disassemble the function?
                // 1. to insert breakpoints at all `ret`
                // 2. to figure out the correct prologue
                //
                // Sometimes, function arguments are passed through registers
                // and so the function prologue is responsible to move those values onto the stack.
                //
                // However, sadly, I am not sure whether it's a Rust specific problem,
                // llvm's prologue location is not always correct.
                // Such that when we first break into the function, some variables are not yet written,
                // causing us capturing garbage.
                // To mitigate, we have to guess the extent of the prologue ourselves.
                if !functions_disassembled.contains_key(&sb_frame.pc()) {
                    let prologue_byte_size = sb_function.prologue_byte_size() as u64;
                    let mut prologue_address =
                        sb_function.start_address().file_address() as u64 + prologue_byte_size;
                    let pc_file_address = sb_frame.pc_address().file_address() as u64;
                    // if we were in the 'designated' prologue
                    let mut try_extend_prologue = pc_file_address == prologue_address
                        // but it only matters if there are arguments
                        && sb_frame
                            .variables(&VariableOptions {
                                arguments: true,
                                locals: false,
                                statics: false,
                                in_scope_only: true,
                            })
                            .len()
                            > 0;
                    if try_extend_prologue {
                        // translate from file address to pc address
                        prologue_address = sb_frame.pc() - pc_file_address + prologue_address;
                    }
                    // println!("pc_address = {}, prologue_address = {}", crate::Addr::new(&sb_frame.pc().to_ne_bytes()), crate::Addr::new(&prologue_address.to_ne_bytes()));
                    let mut extended_prologue = 0;
                    #[allow(unused_variables)]
                    let mut ret_loc = 0;
                    for line in sb_frame.disassemble().unwrap_or_default().lines() {
                        // println!("{line}");
                        let (inst, operand) = if let Some(inst) = line.split(": ").nth(1) {
                            parse_asm(inst.split("; ").nth(0).expect("asm stmt").trim())
                        } else {
                            continue;
                        };
                        if try_extend_prologue {
                            let current_addr = parse_addr(line)?;
                            if current_addr >= prologue_address {
                                // println!("=== {inst:?} {operand:?}");
                                #[cfg(target_arch = "x86_64")]
                                let is_prolog = inst.starts_with("mov")
                                    && (operand.contains("(%rsp)") || operand.contains("(%rbp)"));
                                #[cfg(target_arch = "aarch64")]
                                let is_prolog = matches!(inst, "str" | "stur")
                                    && (operand.contains("[sp,") || operand.contains("[x29,")); // x29 is the 'frame pointer'
                                if !is_prolog {
                                    if inst == RET {
                                        // ooops we reached a return point
                                    } else {
                                        extended_prologue =
                                            (current_addr - prologue_address).try_into()?;
                                    }
                                    try_extend_prologue = false;
                                }
                            }
                        }
                        if inst == RET {
                            if line.split("0x").nth(0).expect("Before addr").contains("->") {
                                // -> means the current instruction
                                return_immediately = true;
                                // there is no need to set a duplicate breakpoint
                            } else {
                                // create breakpoint of type `FunctionReturn`
                                let addr = parse_addr(line)?;
                                log::debug!("Set breakpoint {}", breakpoints.len());
                                let sb_addr = SBAddress::from_load_address(addr, sb_target);
                                let sb_bp = sb_target.breakpoint_create_by_address(&sb_addr);
                                log::debug!("{:#?}", sb_bp);
                                let sb_bp_loc = sb_bp.location_at_index(0);
                                log::debug!("{:#?}", sb_bp_loc);
                                register_breakpoint(breakpoint_addresses, &sb_bp);
                                let bp = Breakpoint {
                                    id: breakpoints.len() as u32,
                                    file_id: bp_file_id,
                                    breakpoint_type: BreakpointType::FunctionReturn,
                                    capture: VariableCapture::None,
                                    ..Default::default()
                                };
                                send_breakpoint(&bp)?;
                                breakpoints.push(bp);
                                ret_loc += 1;
                            }
                        }
                    }
                    functions_disassembled.insert(sb_frame.pc(), FuncAsm { extended_prologue });
                }
                let func_info = functions_disassembled.get(&sb_frame.pc()).expect("Cached");
                if func_info.extended_prologue > 0 {
                    log::debug!(
                        "{} extended_prologue = {}",
                        sb_function.name(),
                        func_info.extended_prologue
                    );
                    let addr = sb_frame.pc() + func_info.extended_prologue as u64;
                    sb_thread
                        .run_to_address(addr)
                        .with_context(|| format!("Fail to run till address: `{addr}`"))?;
                }

                EventStream::function_call(bp_id, thread_id, active_frames.last().expect("Pushed"))
            } else if matches!(bp_event_type, BreakpointType::FutureEndpoint) {
                let sb_function = sb_frame.function();
                let fn_name = sb_function.name();
                let (active_frame_id, reason) = active_frames
                    .last()
                    .map(|active_frame| {
                        (
                            active_frame.frame_id,
                            if fn_name.ends_with("::{{closure}}") {
                                Reason::FutureExit
                            } else {
                                Reason::FutureEnter
                            },
                        )
                    })
                    .unwrap_or_default();
                let mut event = EventStream::breakpoint(bp_id, thread_id, active_frame_id, reason);
                event.write_string(rwriter, "fn", &fn_name.replace("::{{closure}}", ""));
                event
            } else {
                unreachable!();
            };

            let mut write_variable = |var: SBValue| event.write_sb_value(rwriter, &var);

            match bp_capture {
                VariableCapture::Arguments if bp_id == FIREDBG_TRACE_BP_ID => {
                    // Handle `fire::dbg!( ... )`
                    // Read the stringify expression
                    if let Some(name_sb_val) = sb_frame.find_variable("name") {
                        let name_bytes = read_str(&name_sb_val)?;
                        let name = std::str::from_utf8(&name_bytes)?;
                        // Read and write the captured value
                        if let Some(v_sb_val) = sb_frame.find_variable("v") {
                            event.write_sb_value_renamed(rwriter, name, &v_sb_val);
                        }
                    }
                }
                VariableCapture::Arguments if bp_id == EXCHANGE_MALLOC => {
                    let mut frame_idx = 1;
                    while frame_idx < sb_thread.num_frames() {
                        if let Some(fn_name) = sb_frame.display_function_name() {
                            if fn_name == "alloc::boxed::Box<T>::new" {
                                let var = sb_frame.find_variable("x");
                                if var.is_none() {
                                    break;
                                }
                                let var = var.unwrap();
                                // this function is `#[inline(always)]` so each call site is unique
                                if !functions_disassembled.contains_key(&sb_frame.pc()) {
                                    let extended_prologue = 0;
                                    functions_disassembled
                                        .insert(sb_frame.pc(), FuncAsm { extended_prologue });
                                    log::trace!("exchange_malloc at {}", sb_frame.pc());
                                    if var.byte_size() == 0 {
                                        // TODO how to handle zero-sized types?
                                        break;
                                    }
                                    // println!("{:?}", sb_frame.symbol_context()); // is this useful?
                                    for line in sb_frame.disassemble().unwrap_or_default().lines() {
                                        if line.split("0x").nth(0).expect("Addr").contains("->") {
                                            // set a breakpoint after the allocation to obtain the address
                                            let addr = parse_addr(line)?;
                                            if !breakpoint_addresses.contains_key(&addr) {
                                                log::debug!("Set breakpoint {}", breakpoints.len());
                                                let sb_addr =
                                                    SBAddress::from_load_address(addr, sb_target);
                                                let sb_bp = sb_target
                                                    .breakpoint_create_by_address(&sb_addr);
                                                log::debug!("{:#?}", sb_bp);
                                                register_breakpoint(breakpoint_addresses, &sb_bp);
                                                let bp = Breakpoint {
                                                    id: breakpoints.len() as u32,
                                                    file_id: FIREDBG_SOURCE_FILE_ID,
                                                    breakpoint_type: BreakpointType::Breakpoint,
                                                    capture: VariableCapture::Only(vec![
                                                        "exchange_malloc".to_owned(),
                                                    ]),
                                                    ..Default::default()
                                                };
                                                send_breakpoint(&bp)?;
                                                breakpoints.push(bp);
                                            }
                                            break;
                                        }
                                    }
                                }
                                if let Some(ty) = var.type_name() {
                                    if ty.contains('{') {
                                        // this is a closure
                                    } else {
                                        match ty {
                                            "alloc::sync::ArcInner<std::thread::Packet<()>>" => {
                                                // user code can never touch these types
                                            }
                                            _ => {
                                                log::debug!("Allocating for {ty}");
                                                *allocating = Some(ty.to_owned());
                                            }
                                        }
                                    }
                                }
                                break;
                            }
                        } else {
                            break;
                        }
                        sb_frame = sb_thread.set_selected_frame(frame_idx);
                        frame_idx += 1;
                    }
                    return Ok(());
                }
                VariableCapture::Only(only) if bp_file_id == FIREDBG_SOURCE_FILE_ID => {
                    if only.len() == 1 && only[0] == "exchange_malloc" {
                        let rax = sb_frame.find_register({
                            #[cfg(target_arch = "x86_64")]
                            {
                                "rax"
                            }
                            #[cfg(target_arch = "aarch64")]
                            {
                                "x0"
                            }
                        });
                        let addr = read_u64(&rax)?;
                        if allocating.is_some() {
                            log::debug!(
                                "exchange_malloc {} -> {}",
                                crate::Addr::new(&addr.to_ne_bytes()),
                                allocating.as_ref().unwrap(),
                            );
                            let ty_name = allocating.take().unwrap();
                            producer
                                .send_to(
                                    &alloc_stream,
                                    serde_json::to_string(&AllocationBorrowed {
                                        action: AllocAction::Alloc,
                                        address: addr,
                                        type_name: &ty_name,
                                    })?,
                                )
                                .context("Fail to send allocation event")?;
                            allocation.insert(addr, ty_name);
                        }
                    } else {
                        log::warn!("Unhandled breakpoint VariableCapture::Only({only:?})");
                    }
                    return Ok(());
                }
                VariableCapture::Arguments if bp_id == DROP_IN_PLACE => {
                    for var in sb_frame
                        .variables(&VariableOptions {
                            arguments: true,
                            locals: false,
                            statics: false,
                            in_scope_only: true,
                        })
                        .iter()
                    {
                        let addr = var.dereference().address();
                        if addr.is_none() {
                            continue;
                        }
                        let addr = addr.unwrap().load_address(sb_target);
                        if let Some(ty_name) = allocation.remove(&addr) {
                            #[cfg(debug_assertions)]
                            {
                                let sb_type = var.type_();
                                assert!(sb_type.name().contains(&ty_name));
                            }
                            producer
                                .send_to(
                                    &alloc_stream,
                                    serde_json::to_string(&AllocationBorrowed {
                                        action: AllocAction::Drop,
                                        address: addr,
                                        type_name: &ty_name,
                                    })?,
                                )
                                .context("Fail to send allocation event")?;
                            log::debug!(
                                " drop_in_place  {} -> {}",
                                crate::Addr::new(&addr.to_ne_bytes()),
                                ty_name
                            );
                        } else if let Some(ty_name) = allocation.get(&(addr - 0x10)) {
                            // the allocation happens at the base address of the RcBox, but the drop happens at the data address
                            if ty_name.starts_with("alloc::rc::RcBox<")
                                || ty_name.starts_with("alloc::sync::ArcInner<")
                            {
                                let ty_name = allocation.remove(&(addr - 0x10)).unwrap();
                                producer
                                    .send_to(
                                        &alloc_stream,
                                        serde_json::to_string(&AllocationBorrowed {
                                            action: AllocAction::Drop,
                                            address: addr - 0x10,
                                            type_name: &ty_name,
                                        })?,
                                    )
                                    .context("Fail to send allocation event")?;
                                log::debug!(
                                    " drop_in_place  {} -> {}",
                                    crate::Addr::new(&addr.to_ne_bytes()),
                                    ty_name
                                );
                            }
                        }
                        break;
                    }
                    return Ok(());
                }
                VariableCapture::Arguments | VariableCapture::Locals => {
                    for var in sb_frame
                        .variables(&VariableOptions {
                            arguments: matches!(bp_capture, VariableCapture::Arguments),
                            locals: matches!(bp_capture, VariableCapture::Locals),
                            statics: false,
                            in_scope_only: true,
                        })
                        .iter()
                    {
                        if var.name().is_some() {
                            write_variable(var);
                        }
                    }
                }
                VariableCapture::Only(vars) => {
                    for var in vars {
                        if let Some(var) = sb_frame.find_variable(&var) {
                            write_variable(var);
                        }
                    }
                }
                VariableCapture::None => (),
            }

            producer
                .send_to(&event_stream, event)
                .context("Fail to stream event")?;
        }

        *allocating = None; // any intermediate breakpoint will clear this

        if is_function_return || return_immediately {
            // We should have at least one active frame because we're handling a function return
            assert!(!active_frames.is_empty());

            if is_function_return {
                let active_frame = active_frames.last().unwrap();
                if sb_frame.pc() == active_frame.program_counter
                    && sb_frame.sp() == active_frame.stack_pointer
                {
                    // Skip breakpoint handling
                    return Ok(());
                }
            }

            let last_frame = active_frames.pop().expect("Not empty");
            if !return_immediately {
                assert!(sb_frame.sp() >= last_frame.stack_pointer);
            }

            let mut event = EventStream::function_return(bp_id, thread_id, &last_frame);
            if write_return_value(
                &mut event,
                rwriter,
                sb_target,
                sb_process,
                &sb_frame,
                fn_cache.get(&last_frame.function_id).expect("Cached"),
            )
            .is_err()
            {
                event.write_value(rwriter, RETVAL, rwriter.opaque_v().as_bytes());
            }

            producer
                .send_to(&event_stream, event)
                .context("Fail to stream return value")?;
        }

        Ok(())
    };

    let sb_process = process_timer
        .debugger_launch
        .time(|| sb_target.launch_redirected(stdout_path, stderr_path, params.arguments))
        .context("Fail to launch debugger")?;
    let sb_process = unsafe {
        SB_PROCESS = Some(sb_process);
        SB_PROCESS.as_ref().expect("Some")
    };

    let t_debugger_run = process_timer.debugger_run.span();

    loop {
        match sb_process.state() {
            // We hit a breakpoint!
            ProcessState::Stopped => {}
            // The process we're debugging ended, we should shutdown the debugger
            ProcessState::Unloaded
            | ProcessState::Crashed
            | ProcessState::Detached
            | ProcessState::Exited => break,
            // Nothing of interest, we will wait for a breakpoint hit
            ProcessState::Invalid
            | ProcessState::Connected
            | ProcessState::Attaching
            | ProcessState::Launching
            | ProcessState::Running
            | ProcessState::Stepping
            | ProcessState::Suspended => continue,
        }
        let sb_thread = sb_process.selected_thread();
        if matches!(sb_thread.stop_reason(), StopReason::Breakpoint) {
            // Parse the breakpoint index from stop description
            // A typical stop reason looks like `breakpoint 1.1`, we want the first `1` out of it.
            let stop_reason_data_count = sb_thread.stop_reason_data_count();
            assert!(stop_reason_data_count >= 2);
            let breakpoint_id = BpId(sb_thread.stop_reason_data_at_index(0) as u32);
            log::debug!("Stop at Breakpoint {breakpoint_id:?}");
            let selected_thread_id = sb_thread.thread_id();
            let bp_addrs = &mut breakpoint_addresses;
            handle_breakpoint(bp_addrs, sb_process, sb_thread, breakpoint_id)
                .context("Fail to handle breakpoint")?;

            // handle other threads
            for i in 0..sb_process.num_threads() {
                let sb_thread = sb_process.thread_at_index(i);
                if sb_thread.thread_id() == selected_thread_id {
                    continue;
                }
                let sb_frame = sb_thread.selected_frame();
                let pc_file_address = sb_frame.pc_address().file_address() as u64;
                // the other thread is also stopped at a breakpoint
                if let Some(bp_id) = breakpoint_addresses.get(&pc_file_address).copied() {
                    log::debug!("Handle extra breakpoint for {bp_id:?}");
                    let bp_addrs = &mut breakpoint_addresses;
                    handle_breakpoint(bp_addrs, sb_process, sb_thread, bp_id)
                        .context("Fail to handle breakpoint")?;
                }
            }
        } else {
            log::debug!("Stop for some reason {:?}", sb_thread.stop_reason());
        }
        log::trace!("sb_process.resume");
        process_timer
            .process_resume
            .time(|| sb_process.resume())
            .context("Fail to resume debugger")?;
    }

    let exit_code = sb_process.exit_status();

    producer.send_to(
        &StreamKey::new(INFO_STREAM)?,
        serde_json::to_string(&InfoMessage::Exit(ProgExitInfo { exit_code }))?.as_str(),
    )?;

    // Fail to start LLDB
    if exit_code == -1 {
        panic!("Fail to start LLDB: {}", sb_process.exit_description());
    }

    std::mem::drop(t_debugger_run);
    process_timer.debugger_cleanup.time(SBDebugger::terminate);
    log::debug!("Debugger Terminated");

    std::mem::drop(t_global);

    log::debug!("{:#?}", process_timer);
    log::info!(
        "Finished.    target: {}    total: {}",
        process_timer.debugger_run,
        process_timer.global
    );

    unsafe {
        SB_PROCESS = None;
        SB_TARGET = None;
    }

    Ok(())
}

pub(crate) fn get_union_type(ty: &SBType) -> Option<Arc<UnionType>> {
    let mut cache = ENUM_CACHE
        .try_lock()
        .expect("There should be no concurrent access");
    let type_id = ty.id();
    if let Some(info) = cache.get(&type_id) {
        return info.clone();
    }
    let ty_desc = format!("{:?}", ty);
    let info = if ty_desc.starts_with("enum ") {
        // ty is `core::result::Result<A, B>`
        let mut lines = ty_desc.split_once("enum ").expect("checked").1.lines();
        let enum_name = lines.next().expect("enum variants").trim_end_matches(" {");
        Some(Arc::new(UnionType {
            name: trim_type_name(enum_name),
            variants: extract_variants(lines),
        }))
    } else if ty_desc.contains("\nStatic:\nenum ") {
        // ty is `core::result::Result<A, B>::Ok`
        // ```
        // Static:
        // enum core::result::Result<A, B> {
        //   Ok: core::result::Result<A, B>::Ok,
        //   Err: core::result::Result<A, B>::Err
        // }
        // ```
        let mut lines = ty_desc
            .split_once("\nStatic:\nenum ")
            .expect("checked")
            .1
            .lines();
        let enum_name = lines
            .next()
            .expect("static enum variants")
            .trim_end_matches(" {");
        Some(Arc::new(UnionType {
            name: trim_type_name(enum_name),
            variants: extract_variants(lines),
        }))
    } else {
        None
    };
    cache.insert(type_id, info.clone());
    info
}

pub(crate) fn get_sb_type(name: &str) -> Option<SBType> {
    let sb_target = unsafe { SB_TARGET.as_ref().expect("Always") };
    let mut cache = TYPE_CACHE
        .try_lock()
        .expect("There should be no concurrent access");
    if !cache.contains_key(name) {
        cache.insert(name.to_owned(), sb_target.find_first_type(name));
    }
    cache.get(name).expect("Cached").to_owned()
}

pub(crate) fn cache_sb_type(ty: SBType) {
    let mut cache = TYPE_CACHE
        .try_lock()
        .expect("There should be no concurrent access");
    let name = ty.name();
    if !cache.contains_key(name) {
        cache.insert(name.to_owned(), Some(ty));
    }
}

pub(crate) fn sb_value_from_addr(
    name: &str,
    addr: u64,
    sb_type: &SBType,
) -> Result<SBValue, WriteErr> {
    let sb_target = unsafe { SB_TARGET.as_ref().expect("Always") };
    let addr = SBAddress::from_load_address(addr, sb_target);
    let value = sb_target.create_value_from_address(name, &addr, sb_type);
    if value.is_valid() {
        Ok(value)
    } else {
        Err(WriteErr)
    }
}

pub(crate) fn sb_value_from_data(
    name: &str,
    data: &[u64],
    sb_type: &SBType,
) -> Result<SBValue, WriteErr> {
    let sb_target = unsafe { SB_TARGET.as_ref().expect("Always") };
    let sb_data = SBData::from_u64(
        data,
        sb_target.byte_order(),
        sb_target.address_byte_size() as u32,
    );
    if !sb_data.is_valid() {
        return Err(WriteErr);
    }
    let value = sb_target.create_value_from_data(name, &sb_data, sb_type);
    if value.is_valid() {
        Ok(value)
    } else {
        Err(WriteErr)
    }
}

pub(crate) fn read_process_memory(addr: u64, len: usize) -> Result<Vec<u8>, WriteErr> {
    let sb_process = unsafe { SB_PROCESS.as_ref().expect("Some") };
    let mut buf = vec![0u8; len];
    sb_process
        .read_memory(addr, &mut buf)
        .map_err(|_| WriteErr)?;
    Ok(buf)
}

fn extract_variants(lines: std::str::Lines) -> Vec<String> {
    lines
        .map_while(|line| {
            if line == "}" {
                None
            } else {
                Some(match line.trim_start().split_once(':') {
                    Some((var, _)) => var.to_owned(),
                    None => line.to_owned(),
                })
            }
        })
        .collect()
}

impl Bytes {
    /// If encounter error, writes opaque
    fn write_sb_value(&mut self, rwriter: &mut RValueWriter, value: &SBValue) {
        let name = value.name().unwrap_or("<>");
        self.write_sb_value_renamed(rwriter, name, value);
    }

    fn write_sb_value_renamed(&mut self, rwriter: &mut RValueWriter, name: &str, value: &SBValue) {
        self.identifier(name);
        self.push_str("name");
        self.space();
        self.write_inner_value(rwriter, value);
        self.space();
    }

    fn write_inner_value(&mut self, rwriter: &mut RValueWriter, value: &SBValue) {
        // write value would call set_env
        if let Ok(val) = rwriter.write_value(value) {
            let env = rwriter.emit_env();
            // env comes first
            self.push_bytes(env);
            self.push_bytes(val);
        } else {
            self.push_bytes(rwriter.opaque_v());
        }
    }

    fn write_value(&mut self, rwriter: &mut RValueWriter, name: &str, val: &[u8]) {
        self.identifier(name);
        self.push_str("name");
        self.space();
        let env = rwriter.emit_env();
        // env comes first
        self.push_bytes(env);
        self.push_slice(val);
        self.space();
    }

    pub fn write_unit_v(&mut self, name: &str) {
        let temp = Default::default();
        let mut rwriter = RValueWriter::new(&temp);
        let bytes = rwriter.unit_v();
        self.write_value(&mut rwriter, name, bytes.as_bytes());
    }

    pub fn write_opaque_v(&mut self, name: &str) {
        let temp = Default::default();
        let mut rwriter = RValueWriter::new(&temp);
        let bytes = rwriter.opaque_v();
        self.write_value(&mut rwriter, name, bytes.as_bytes());
    }

    fn write_result(
        &mut self,
        rwriter: &mut RValueWriter,
        name: &str,
        ty: &str,
        ok: bool,
        val: Bytes,
    ) {
        let result_enum = UnionType {
            name: trim_type_name(ty),
            variants: vec!["Ok".to_owned(), "Err".to_owned()],
        };
        let bytes = rwriter.union_v(
            &result_enum,
            if ok { 0 } else { 1 },
            [("0".to_owned(), val)].into_iter(),
        );
        self.write_value(rwriter, name, bytes.as_bytes());
    }

    fn write_option(
        &mut self,
        rwriter: &mut RValueWriter,
        name: &str,
        ty: &str,
        val: Option<Bytes>,
    ) {
        let option_enum = UnionType {
            name: trim_type_name(ty),
            variants: vec!["None".to_owned(), "Some".to_owned()],
        };
        let bytes = if let Some(val) = val {
            rwriter.union_v(&option_enum, 1, [("0".to_owned(), val)].into_iter())
        } else {
            rwriter.union_v(&option_enum, 0, [].into_iter())
        };
        self.write_value(rwriter, name, bytes.as_bytes());
    }

    fn write_string(&mut self, rwriter: &mut RValueWriter, name: &str, value: &str) {
        self.identifier(name);
        self.push_str("name");
        self.space();
        self.push_bytes(rwriter.strlit_v(value.as_bytes()));
        self.space();
    }
}

impl Default for Thread {
    fn default() -> Self {
        Self {
            frame: Frame { frame_id: 0 },
            active_frames: Vec::new(),
            allocating: None,
        }
    }
}

#[doc(hidden)]
pub fn new_breakpoint(id: u32, file_id: u32, func: &FunctionDef) -> Breakpoint {
    Breakpoint {
        id,
        file_id,
        loc: func.loc.start,
        loc_end: Some(func.loc.end),
        breakpoint_type: BreakpointType::FunctionCall {
            fn_name: func.ty.fn_name().into(),
        },
        capture: VariableCapture::Arguments,
    }
}

#[doc(hidden)]
pub fn new_async_breakpoint(id: u32, file_id: u32, func: &FunctionDef) -> Breakpoint {
    Breakpoint {
        id,
        file_id,
        loc: func.end,
        loc_end: None,
        breakpoint_type: BreakpointType::FutureEndpoint,
        capture: VariableCapture::None,
    }
}

fn register_breakpoint(breakpoint_addresses: &mut FxHashMap<u64, BpId>, sb_bp: &SBBreakpoint) {
    for sb_bp_loc in sb_bp.locations() {
        breakpoint_addresses.insert(sb_bp_loc.address().file_address() as u64, BpId(sb_bp.id()));
    }
}

fn get_rust_panic_source_file() -> SourceFile {
    SourceFile {
        id: FIREDBG_SOURCE_FILE_ID,
        path: "FireDBG.Internal".into(),
        crate_name: "FireDBG.Internal".into(),
        modified: SystemTime::now(),
    }
}

fn get_rust_panic_breakpoint() -> Breakpoint {
    Breakpoint {
        id: RUST_PANIC_BP_ID.0,
        file_id: FIREDBG_SOURCE_FILE_ID,
        loc: Default::default(),
        loc_end: Default::default(),
        breakpoint_type: BreakpointType::Breakpoint,
        capture: VariableCapture::Locals,
    }
}

fn get_firedbg_trace_breakpoint() -> Breakpoint {
    Breakpoint {
        id: FIREDBG_TRACE_BP_ID.0,
        file_id: FIREDBG_SOURCE_FILE_ID,
        loc: Default::default(),
        loc_end: Default::default(),
        breakpoint_type: BreakpointType::Breakpoint,
        capture: VariableCapture::Arguments,
    }
}

fn get_exchange_malloc_breakpoint() -> Breakpoint {
    Breakpoint {
        id: EXCHANGE_MALLOC.0,
        file_id: FIREDBG_SOURCE_FILE_ID,
        loc: Default::default(),
        loc_end: Default::default(),
        breakpoint_type: BreakpointType::Breakpoint,
        capture: VariableCapture::Arguments,
    }
}

fn get_drop_in_place_breakpoint() -> Breakpoint {
    Breakpoint {
        id: DROP_IN_PLACE.0,
        file_id: FIREDBG_SOURCE_FILE_ID,
        loc: Default::default(),
        loc_end: Default::default(),
        breakpoint_type: BreakpointType::Breakpoint,
        capture: VariableCapture::Arguments,
    }
}

fn parse_addr(line: &str) -> Result<u64> {
    // A typical line would be `    0x100003d4c <+60>:  retq`,
    // we only want `100003d4c` out of it.
    let addr = line
        .split("0x")
        .nth(1)
        .expect("Starts with `0x`")
        .split_ascii_whitespace()
        .nth(0)
        .expect("Read until space");
    let res =
        u64::from_str_radix(addr, 16).with_context(|| format!("Invalid address: `{addr}`"))?;
    Ok(res)
}

fn read_u64(v: &SBValue) -> Result<u64, WriteErr> {
    v.data().read_u64(0).map_err(|_| WriteErr)
}

#[doc(hidden)]
pub fn get_target_basename(output: &str) -> &str {
    output.trim_end_matches(".firedbg.ss")
}

/// The same `rustc` version must be used to compile the debugger and the target executable.
/// This is due to implicit ABI dependency everywhere.
pub fn check_rustc_version() {
    let rustc_version = rustc_version();
    if rustc_version != SUPPORTED_RUSTC_VERSION {
        panic!("Mismatched `rustc` version: expect `{SUPPORTED_RUSTC_VERSION}`, found `{rustc_version}`. Please install a matching FireDBG version.");
    }
}
